package ru.aslanov.schedule.server;

import ru.aslanov.schedule.model.*;
import ru.aslanov.schedule.utils.MemCacheUtil;
import ru.aslanov.schedule.utils.XMLUtil;

import javax.jdo.PersistenceManager;
import javax.jdo.Transaction;
import javax.jdo.annotations.PersistenceAware;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.*;
import java.util.logging.Logger;

/**
 * Created by IntelliJ IDEA.
 * Created: Feb 2, 2010 10:34:06 AM
 *
 * @author Sergey Aslanov
 */
@PersistenceAware
public abstract class AbstractService<T extends Entity> {
    private static final Logger log = Logger.getLogger(AbstractService.class.getName());
    private static final DatatypeFactory DATATYPE_FACTORY;
    static {
        try {
            DATATYPE_FACTORY = DatatypeFactory.newInstance();
        } catch (DatatypeConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    private List<BackReference> backReferences = new ArrayList<BackReference>();

    protected abstract Class<T> getEntityClass();

    protected abstract Object doFetch(HttpServletRequest request) throws Exception;

    protected void addBackRef(BackReference backReference) {
        backReferences.add(backReference);
    }

    protected void fillDataItem(DataItem dataItem, T object, HttpServletRequest request, PersistenceManager pm) throws Exception {
        setAttr(dataItem, "encodedKey", object.getEncodedKey());
    }

    protected DataList<DataItem> convertToDataList(Collection objects, HttpServletRequest request, PersistenceManager pm) throws Exception {
        DataList<DataItem> dataList = new DataList<DataItem>();
        for (Object object : objects) {
            DataItem dataItem = new DataItem();
            fillDataItem(dataItem, (T) object, request, pm);
            dataList.addItem(dataItem);
        }
        return dataList; 
    }

    protected void setAttr(DataItem dataItem, String attrName, Object value) throws Exception {
        if (value != null) {
            dataItem.setAttribute(attrName, object2String(value));
        }
    }

    public void service(OperationEnum operationEnum, HttpServletRequest request, HttpServletResponse response) throws Exception {
        InputLangUtil.setThreadLocalInputLang(request.getParameter("inputLang"), getDefaultInputLang(request));
        try {
            switch (operationEnum) {
                case FETCH:
                    fetch(request, response);
                    break;
                case ADD:
                case UPDATE:
                    update(request, response);
                    break;
                case REMOVE:
                    remove(request, response);
                    break;
                default:
                    throw new RuntimeException("Unknown operation: " + operationEnum);
            }
        } finally {
            InputLangUtil.clearThreadLocalInputLang();
        }
    }

    protected abstract String getDefaultInputLang(HttpServletRequest request);

    private void fetch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Object o = doFetch(request);

        marshalObjectToResponse(response, o);
    }

    protected void setParameter(T object, String paramName, String paramValue, PersistenceManager pm) throws Exception {
        final String setterName = "set" + Character.toUpperCase(paramName.charAt(0)) + paramName.substring(1);
        try {
            log.finest("Looking for setter " + setterName);
            final Method method = getMethod(object, setterName);
            if (method.getParameterTypes().length != 1) {
                throw new RuntimeException("Setter " + object.getClass().getName() + "." + setterName + " should have one param");
            }
            final Class<?> propType = method.getParameterTypes()[0];

            Object propValue = string2object(propType, paramValue, pm);

            method.invoke(object, propValue);
        } catch (NoSuchMethodException e) {
            log.warning("Setter is not found: " + object.getClass().getName() + "." + setterName);
        }
    }

    protected void setProperties(PersistenceManager pm, T object, HttpServletRequest request) throws Exception {
        final Enumeration paramNames = request.getParameterNames();
        log.finest("Setting properties, has params? " + paramNames.hasMoreElements());
        while (paramNames.hasMoreElements()) {
            String param = (String) paramNames.nextElement();
            if ("encodedKey".equals(param)) continue;
            final String paramValue = request.getParameter(param);
            log.finest("Setting property " + param + " = " + (paramValue != null ? paramValue : "-null-"));

            setParameter(object, param, paramValue, pm);
        }
    }

    private Method getMethod(T object, String methodName) throws NoSuchMethodException {
        Method found = null;
        for (Method method : object.getClass().getMethods()) {
            if (method.getName().equals(methodName)) {
                if (found != null) {
                    throw new RuntimeException("Found more that one method '" + method + "'");
                }
                found = method;
            }
        }
        if (found == null) {
            throw new NoSuchMethodException("Method " + object.getClass().getName() + "." + methodName + " is not found");
        }
        return found;
    }


    private String object2String(Object value) throws Exception {
        String str;
        if (value == null) {
            return null;
        } else  if (value instanceof Entity) {
            Entity entity = (Entity) value;
            str = entity.getEncodedKey();
        } else if (value instanceof Date) {            
            final XMLGregorianCalendar gregorianCalendar = new XmlDate2DateAdapter().marshal((Date) value);
            str = gregorianCalendar.toXMLFormat();
        } else {
            str = value.toString();
        }
        return str;
    }

    protected Object string2object(Class<?> propType, String paramValue, PersistenceManager pm) throws Exception {
        log.finest("Calculating value for type: " + propType + " value: " + paramValue);
        if (paramValue == null || "".equals(paramValue) || "null".equals(paramValue)) {
            return null;
        } else if (propType == String.class) {
            return paramValue;
        } else if (propType == Integer.class || propType == Integer.TYPE) {
            return new Integer(paramValue);
        } else if (propType == Long.class || propType == Long.TYPE) {
            return new Long(paramValue);
        } else if (propType == Boolean.class || propType == Boolean.TYPE) {
            return Boolean.valueOf(paramValue);
        } else if (propType == Date.class) {
            final XMLGregorianCalendar gregorianCalendar = DATATYPE_FACTORY.newXMLGregorianCalendar(paramValue);
            final Date date = new XmlDate2DateAdapter().unmarshal(gregorianCalendar);
            return  date;
        } else if (propType.isEnum()) {
            return Enum.valueOf((Class<? extends Enum>) propType, paramValue);
        } else if (Entity.class.isAssignableFrom(propType)) {
            return pm.getObjectById(propType, paramValue);
        } else {
            throw new RuntimeException("Cannot process field for type: " + propType);
        }
    }

    private void update(HttpServletRequest request, HttpServletResponse response) throws Exception {
        final PersistenceManager pm = getPersistanceManager();

        final Transaction tran = pm.currentTransaction();

        try {
            clearExecuteAfterCommit();
            T object = findExistingObject(request, pm);
            boolean isNew = object == null;

            if (!checkPermission(object, request, pm, isNew ? Operation.CREATE : Operation.UPDATE)) {
                throw new AccessDeniedException(request);
            }

            if (object == null) {
                object = getEntityClass().newInstance();
            }

            tran.begin();

            if (!isNew) onChange(pm, object, Operation.UPDATE, request);

            setProperties(pm, object, request);

            if (isNew) {
                makeObjectPersistent(pm, object, request);

                onChange(pm, object, Operation.CREATE, request);
            }
            tran.commit();

            final String res = marshalObject(convertToDataList(Collections.singleton(object), request, pm));

            if (!isNew) {
                MemCacheUtil.removeEntityFromCache(object.getEncodedKey());
            }

            performExecuteAfterCommit();

            writeResponse(response, res);            
        } finally {
            if (tran.isActive()) tran.rollback();
            //pm.close();
        }
    }

    protected void onChange(PersistenceManager pm, T object, Operation operation, HttpServletRequest request) { }

    protected abstract boolean checkPermission(T existingEntity, HttpServletRequest request, PersistenceManager pm, Operation operation) throws AccessDeniedException;

    protected abstract void makeObjectPersistent(PersistenceManager pm, T object, HttpServletRequest request); 

    protected T findExistingObject(HttpServletRequest request, PersistenceManager pm) {
        T bean = null;
        final String key = request.getParameter("encodedKey");
        log.finer("Looking for object with key " + key);
        if (key != null) {
            bean = pm.getObjectById(getEntityClass(), key);
        }
        return bean;
    }


    private void remove(HttpServletRequest request, HttpServletResponse response) throws Exception {
        final PersistenceManager pm = getPersistanceManager();
        final Transaction tran = pm.currentTransaction();
        try {
            clearExecuteAfterCommit();

            T object = findExistingObject(request, pm);
            String encodedKey = object.getEncodedKey();

            if (!checkPermission(object, request, pm, Operation.REMOVE)) {
                throw new AccessDeniedException(request);
            }

            canDelete(object);

            final String res = marshalObject(convertToDataList(Collections.singleton(object), request, pm));

            tran.begin();

            onChange(pm, object, Operation.REMOVE, request);

            pm.deletePersistent(object);

            tran.commit();

            MemCacheUtil.removeEntityFromCache(encodedKey);

            performExecuteAfterCommit();

            writeResponse(response, res);
        } finally {
            if (tran.isActive()) tran.rollback();
            //pm.close();
        }
    }

    private void canDelete(T object) throws Exception {
        List<BackReference> violatedBackRefs = new ArrayList<BackReference>();
        for (BackReference backReference : backReferences) {
            if (!backReference.check(object)) {
                violatedBackRefs.add(backReference);
            }
        }
        if (violatedBackRefs.size() > 0) {
            throw new CannotDeleteHasBackRefException(violatedBackRefs);
        }
    }

    private static void marshalObjectToResponse(HttpServletResponse response, Object o) throws JAXBException, IOException {
        final String s = marshalObject(o);
        writeResponse(response, s);
    }

    private static void writeResponse(HttpServletResponse response, String s) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(s);
    }

    private static String marshalObject(Object o) throws JAXBException {
        final Marshaller marshaller = XMLUtil.JAXB_CONTEXT.createMarshaller();

        StringWriter writer = new StringWriter();
        marshaller.marshal(o, writer);
        return writer.toString();
    }


    protected PersistenceManager getPersistanceManager() {
        return PMF.getThreadLocalPersistenceManager();
    }

    private ThreadLocal<Runnable> executeAfterCommitThreadLocal =  new ThreadLocal<Runnable>();

    protected void executeAfterCommit(Runnable runnable) {
        executeAfterCommitThreadLocal.set(runnable);
    }
    private void clearExecuteAfterCommit() {
        executeAfterCommitThreadLocal.remove();
    }
    private void performExecuteAfterCommit() {
        final Runnable runnable = executeAfterCommitThreadLocal.get();
        if (runnable != null) {
            clearExecuteAfterCommit();
            runnable.run();
        }
    }
}
